package heartbeat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class TcpClient 
{
	private SocketChannel mSocketChannel;
	private PCallback<MessageCBType> mOnAuthenticationCallback;
	private PCallback<MessageCBType> mOnMessageCallback;
	private PCallback<ChannelsCBType> mOnChannelsReceivedCallback;
	private Queue<ChannelMessagePair> mChannelMessageQueue;
	private Map<String, Integer> mChannelMap;
	private static final int LIBRARY_CHANNEL_SUBSCRIPTION = 3;
	private static final int HEADER_LENGTH = 4;
	private static final int CHANNEL_LENGTH = 4;
	public TcpClient(String address, int port) throws IOException
	{
		mSocketChannel = SocketChannel.open();
		mSocketChannel.connect(new InetSocketAddress(address, port));
		mSocketChannel.configureBlocking(false);
		mChannelMessageQueue = new LinkedList<ChannelMessagePair>();
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized String channelName(int channelNumber)
	{
		if(!channelMapValid())
			return null;
		
		//Go through the map to try and find the String that maps to this integer.
		for(Map.Entry<String, Integer> entry : mChannelMap.entrySet())
		{
			if(entry.getValue() != channelNumber)
				continue;
			
			return entry.getKey();
		}
		
		return null;
	}
	
	protected synchronized Integer channelNumber(String channelName)
	{
		if(!channelMapValid())
			return null;
		
		if(!mChannelMap.containsKey(channelName))
			return null;
		
		return mChannelMap.get(channelName);
	}
	
	public void close()
	{
		try 
		{
			mSocketChannel.close();
		}
		catch (IOException e) {}
	}
	
	protected void addChannelMessageToQueue(String pChannelName, JSONObject pMessage)
	{
		mChannelMessageQueue.add(new ChannelMessagePair(pChannelName, pMessage));
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized boolean channelMapValid()
	{
		return mChannelMap != null;
	}
	
	protected void emptyChannelMessageQueue()
	{
		while(!mChannelMessageQueue.isEmpty())
		{
			ChannelMessagePair pChannelMessagePair = mChannelMessageQueue.poll();
			write(pChannelMessagePair.channelName(), pChannelMessagePair.message());
		}
	}
	
	//This needs to be synchronized since the channel map can be used from both threads.
	protected synchronized void fillChannelMap(JSONObject pChannelList)
	{
		mChannelMap = new HashMap<String, Integer>();
		try
		{
			JSONArray channels = pChannelList.getJSONArray("channels");
			for(int i = 0; i < channels.length(); ++i)
			{
				JSONObject channel = channels.getJSONObject(i);
				String channelName = channel.names().getString(0);
				mChannelMap.put(channelName, channel.getInt(channelName));
			}
		}
		//Malformed channel list, Just return.
		catch(JSONException e)
		{
			return;
		}
	}
	
	public synchronized void invokeOnChannelsReceivedCallback()
	{
		if(mOnChannelsReceivedCallback == null)
			return;
		
		if(mChannelMap == null)
			return;
		
		List<String> pChannelList = new ArrayList<String>();
		for(String channel : mChannelMap.keySet())
			pChannelList.add(channel);
		
		ChannelsCBType cb = new ChannelsCBType(this, pChannelList);
		
		mOnChannelsReceivedCallback.invoke(cb);
	}
	
	public void invokeOnAuthenticationCallback(MessageCBType message)
	{
		if(mOnAuthenticationCallback == null)
			return;
		
		mOnAuthenticationCallback.invoke(message);
	}
	
	public void invokeOnMessageCallback(MessageCBType message)
	{
		if(mOnMessageCallback == null)
			return;
		
		mOnMessageCallback.invoke(message);
	}
	
	public Packet read()
	{
		if(!mSocketChannel.isConnected())
			return null;
		
		int readLength = HEADER_LENGTH + CHANNEL_LENGTH;
		ByteBuffer buf = ByteBuffer.allocate(readLength);
		int bytesRemaining = readLength;
		while(true)
		{
			long bytesRead;
			try 
			{
				bytesRead = mSocketChannel.read(buf);
			} 
			catch (IOException e) 
			{
				return null;
			}
			
			if(bytesRead <= 0)
				return null;
			
			bytesRemaining -= bytesRead;
			if(bytesRemaining == 0)
				break;
		}
		
		byte[] bufArray = buf.array();

		//Split the header into body length and channel.
		byte[] bodyLenByteArr = Arrays.copyOfRange(bufArray, 0, HEADER_LENGTH);
		byte[] channelByteArr = Arrays.copyOfRange(bufArray, HEADER_LENGTH, HEADER_LENGTH + CHANNEL_LENGTH);
		
		int bodyLength = ByteBuffer.wrap(bodyLenByteArr).order(ByteOrder.LITTLE_ENDIAN).getInt();
		int channel = ByteBuffer.wrap(channelByteArr).order(ByteOrder.LITTLE_ENDIAN).getInt();
		
		//Now read the body.
		buf = ByteBuffer.allocate(bodyLength);
		bytesRemaining = bodyLength;
		while(true)
		{
			long bytesRead;
			try 
			{
				bytesRead = mSocketChannel.read(buf);
			} 
			catch (IOException e) 
			{
				return null;
			}

			bytesRemaining -= bytesRead;
			if(bytesRemaining == 0)
				break;
		}
		
		//Flip the buffer.
		buf.flip();
		String bodyStr = byteBufferToString(buf).trim();
		if(bodyStr == null)
			return null;
		
		return new Packet(channel, PacketUtils.stringToJSON(bodyStr));
	}
	
	protected void setOnAuthenticationCallback(PCallback<MessageCBType> pCallback)
	{
		mOnAuthenticationCallback = pCallback;
	}
	
	protected void setOnChannelsReceivedCallback(PCallback<ChannelsCBType> pCallback)
	{
		mOnChannelsReceivedCallback = pCallback;
	}
	
	
	protected void setOnMessageCallback(PCallback<MessageCBType> pCallback) 
	{
		mOnMessageCallback = pCallback;
	}
	
	protected void sendAuthentication(JSONObject authData)
	{
		write("Library_Authentication", authData);
	}
	
	protected void subscribeTo(String channelName)
	{
		List<String> channelNameList = new ArrayList<String>();
		channelNameList.add(channelName);
		subscribeTo(channelNameList);
	}
	
	protected synchronized void subscribeTo(List<String> channelNames)
	{
		JSONObject outMessage = new JSONObject();
		JSONArray channelListJson = new JSONArray();
		for(String cName : channelNames)
		{
			if(!mChannelMap.containsKey(cName))
				continue;
			
			int channelNumber = mChannelMap.get(cName);
			channelListJson.put(channelNumber);
		}
		
		try 
		{
			outMessage.put("channels", channelListJson);
		} 
		catch (JSONException e) 
		{
			return;
		}
		
		write(new Packet(LIBRARY_CHANNEL_SUBSCRIPTION, outMessage));
	}
	
	
	protected void write(Packet msg)
	{
		String msgStr = msg.message().toString();
		
		ByteBuffer buf = ByteBuffer.allocate(HEADER_LENGTH + CHANNEL_LENGTH + msgStr.length());
		buf.order(ByteOrder.LITTLE_ENDIAN);
		buf.clear();
		buf.putInt(msgStr.length());
		buf.putInt(msg.channel());
		buf.put(msgStr.getBytes());
		buf.flip();
		while(buf.hasRemaining())
		{
			try
			{
				mSocketChannel.write(buf);
			}
			catch(IOException e) 
			{
				return;
			}
		}
	}
	
	protected synchronized void write(String channelName, JSONObject message)
	{
		if(!channelMapValid())
			return;
		
		Integer channelNumber;
		if((channelNumber = mChannelMap.get(channelName)) == null)
			return;
		
		Packet pPacket = new Packet(channelNumber, message);
		write(pPacket);
	}
	
	private String byteBufferToString(ByteBuffer buf)
	{
		 Charset charset = Charset.forName( "UTF-8" );  
         CharsetDecoder decoder = charset.newDecoder();
         CharBuffer cb;
		try 
		{
			cb = decoder.decode(buf);
		} 
		catch (CharacterCodingException e) 
		{
			return null;
		}
		
		return cb.toString();
	}
}
